package com.urick.enums;


import com.urick.constants.Constants;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * Design Theories: 灵感来源于 {@link Constants}
 * <a href=""></a>
 * 基于枚举常量配置转换工具类，自定义实现，具备更好的语义；
 * 推荐更简单的{@link Constants}
 *
 * @author Rick 2022/12/7 15:30
 * Email: rickwork@163.com
 * Wechat: yy3076815642
 * QQ: 3076815642
 * @since 1.0
 */
public class Enums {

    /**
     * The name of the introspected class
     */
    private final String className;

    /**
     * Map from String field name to object value
     */
    private final Map<String, IEnum<?>> fieldCache = new HashMap<>();


    /**
     * Create a new Enum converter class wrapping the given class.
     * <p>All <b>public</b> static final variables will be exposed, whatever their type.
     *
     * @param clazz the class to analyze
     * @throws IllegalArgumentException if the supplied {@code clazz} is {@code null}
     */
    public Enums(Class<? extends IEnum<?>> clazz) {
        if (clazz == null) {
            throw new EnumException("Class must not be null");
        }
        if (!clazz.isEnum()) {
            throw new EnumException("Class must be implements " + IEnum.class.getSimpleName() + " Enum type");
        }
        Set<Object> checkValue = new HashSet<>();
        this.className = clazz.getName();
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            if (Modifier.isPrivate(field.getModifiers())) {
                field.setAccessible(true);
            }
            try {
                String name = field.getName();
                IEnum<?> value = (IEnum<?>) field.get(null);
                if (checkValue.contains(value.getValue())) {
                    throw new EnumException("Duplicate enumeration [" + IEnum.class.getSimpleName() + "] values [" + name + "->" + value.getValue() + "] are declared ");
                }
                this.fieldCache.put(name, value);
                checkValue.add(value.getValue());
            } catch (IllegalAccessException ex) {
                // just leave this field and continue
            }
        }
    }


    /**
     * Return the name of the analyzed class.
     */
    public final String getClassName() {
        return this.className;
    }

    /**
     * Return the number of enums exposed.
     */
    public final int getSize() {
        return this.fieldCache.size();
    }

    /**
     * Exposes the field cache to subclasses:
     * a Map from String field name to object value.
     */
    protected final Map<String, IEnum<?>> getFieldCache() {
        return this.fieldCache;
    }


    /**
     * Return a enums value cast to a Number.
     *
     * @param name the name of the field (never {@code null})
     * @return the Number value
     * @throws EnumException if the field name wasn't found
     *                       or if the type wasn't compatible with Number
     * @see #asObject
     */
    public Number asNumber(String name) throws EnumException {
        IEnum<?> obj = asObject(name);
        if (!(obj.getValue() instanceof Number)) {
            throw new EnumException(this.className, name, "not a Number");
        }
        return (Number) obj.getValue();
    }

    /**
     * Return a enums value as a String.
     *
     * @param name the name of the field (never {@code null})
     * @return the String value
     * Works even if it's not a string (invokes {@code toString()}).
     * @throws EnumException if the field name wasn't found
     * @see #asObject
     */
    public String asString(String name) throws EnumException {
        IEnum<?> obj = asObject(name);
        if (!(obj.getValue() instanceof String)) {
            throw new EnumException(this.className, name, "not a String");
        }
        return obj.getValue().toString();
    }

    /**
     * Parse the given String (upper or lower case accepted) and return
     * the appropriate value if it's the name of a enums field in the
     * class that we're analysing.
     *
     * @param name the name of the field (never {@code null})
     * @return the Object value
     * @throws EnumException if there's no such field
     */
    public IEnum<?> asObject(String name) throws EnumException {
        if (name == null) {
            throw new EnumException("name must not be null");
        }
        String nameToUse = name.toUpperCase(Locale.ENGLISH);
        IEnum<?> val = this.fieldCache.get(nameToUse);
        if (val == null) {
            throw new EnumException(this.className, nameToUse, "not found");
        }
        return val;
    }

    public IEnum<?> asObject(Object value) throws EnumException {
        if (value == null) {
            throw new EnumException("value must not be null");
        }
        IEnum<?> val = null;
        for (Map.Entry<String, IEnum<?>> iEnumEntry : this.fieldCache.entrySet()) {
            if (iEnumEntry.getValue().getValue().equals(value)) {
                val = iEnumEntry.getValue();
            }
        }
        if (val == null) {
            throw new EnumException(this.className, value);
        }
        return val;
    }


    /**
     * Return all names of the given group of enums.
     * <p>Note that this method assumes that enums are named
     * in accordance with the standard Java convention for enums
     * values (i.e. all uppercase). The supplied {@code namePrefix}
     * will be uppercased (in a locale-insensitive fashion) prior to
     * the main logic of this method kicking in.
     *
     * @param namePrefix prefix of the enums names to search (may be {@code null})
     * @return the set of enums names
     */
    public Set<String> getNames(String namePrefix) {
        String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : "");
        Set<String> names = new HashSet<>();
        for (String name : this.fieldCache.keySet()) {
            if (name.startsWith(prefixToUse)) {
                names.add(name);
            }
        }
        return names;
    }

    /**
     * Return all names of the group of enums for the
     * given bean property name.
     *
     * @param propertyName the name of the bean property
     * @return the set of values
     * @see #propertyToConstantNamePrefix
     */
    public Set<String> getNamesForProperty(String propertyName) {
        return getNames(propertyToConstantNamePrefix(propertyName));
    }

    /**
     * Return all names of the given group of enums.
     * <p>Note that this method assumes that enums are named
     * in accordance with the standard Java convention for enums
     * values (i.e. all uppercase). The supplied {@code nameSuffix}
     * will be uppercased (in a locale-insensitive fashion) prior to
     * the main logic of this method kicking in.
     *
     * @param nameSuffix suffix of the enums names to search (may be {@code null})
     * @return the set of enums names
     */
    public Set<String> getNamesForSuffix(String nameSuffix) {
        String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : "");
        Set<String> names = new HashSet<>();
        for (String name : this.fieldCache.keySet()) {
            if (name.endsWith(suffixToUse)) {
                names.add(name);
            }
        }
        return names;
    }


    /**
     * Return all values of the given group of enums.
     * <p>Note that this method assumes that enums are named
     * in accordance with the standard Java convention for enums
     * values (i.e. all uppercase). The supplied {@code namePrefix}
     * will be uppercased (in a locale-insensitive fashion) prior to
     * the main logic of this method kicking in.
     *
     * @param namePrefix prefix of the enums names to search (may be {@code null})
     * @return the set of values
     */
    public Set<Object> getValues(String namePrefix) {
        String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : "");
        Set<Object> values = new HashSet<>();
        for (String name : this.fieldCache.keySet()) {
            if (name.startsWith(prefixToUse)) {
                values.add(this.fieldCache.get(name).getValue());
            }
        }
        return values;
    }

    /**
     * Return all values of the group of enums for the
     * given bean property name.
     *
     * @param propertyName the name of the bean property
     * @return the set of values
     * @see #propertyToConstantNamePrefix
     */
    public Set<Object> getValuesForProperty(String propertyName) {
        return getValues(propertyToConstantNamePrefix(propertyName));
    }

    /**
     * Return all values of the given group of enums.
     * <p>Note that this method assumes that enums are named
     * in accordance with the standard Java convention for enums
     * values (i.e. all uppercase). The supplied {@code nameSuffix}
     * will be uppercased (in a locale-insensitive fashion) prior to
     * the main logic of this method kicking in.
     *
     * @param nameSuffix suffix of the enums names to search (may be {@code null})
     * @return the set of values
     */
    public Set<Object> getValuesForSuffix(String nameSuffix) {
        String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : "");
        Set<Object> values = new HashSet<>();
        for (String name : this.fieldCache.keySet()) {
            if (name.endsWith(suffixToUse)) {
                values.add(this.fieldCache.get(name));
            }
        }
        return values;
    }


    /**
     * Look up the given value within the given group of enums.
     * <p>Will return the first match.
     *
     * @param iEnum      enums value to look up
     * @param namePrefix prefix of the enums names to search (may be {@code null})
     * @return the name of the enums field
     * @throws EnumException if the value wasn't found
     */
    public String toName(IEnum<?> iEnum, String namePrefix) throws EnumException {
        String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : "");
        for (Map.Entry<String, IEnum<?>> entry : this.fieldCache.entrySet()) {
            if (entry.getKey().startsWith(prefixToUse) && entry.getValue().getValue().equals(iEnum.getValue())) {
                return entry.getKey();
            }
        }
        throw new EnumException(this.className, prefixToUse, iEnum);
    }

    /**
     * Look up the given value within the group of enums for
     * the given bean property name. Will return the first match.
     *
     * @param iEnum        enums value to look up
     * @param propertyName the name of the bean property
     * @return the name of the enums field
     * @throws EnumException if the value wasn't found
     * @see #propertyToConstantNamePrefix
     */
    public String toNameForProperty(IEnum<?> iEnum, String propertyName) throws EnumException {
        return toName(iEnum, propertyToConstantNamePrefix(propertyName));
    }

    /**
     * Look up the given value within the given group of enums.
     * <p>Will return the first match.
     *
     * @param iEnum      enums value to look up
     * @param nameSuffix suffix of the enums names to search (may be {@code null})
     * @return the name of the enums field
     * @throws EnumException if the value wasn't found
     */
    public String toNameForSuffix(IEnum<?> iEnum, String nameSuffix) throws EnumException {
        String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : "");
        for (Map.Entry<String, IEnum<?>> entry : this.fieldCache.entrySet()) {
            if (entry.getKey().endsWith(suffixToUse) && entry.getValue().getValue().equals(iEnum.getValue())) {
                return entry.getKey();
            }
        }
        throw new EnumException(this.className, suffixToUse, iEnum);
    }


    /**
     * Convert the given bean property name to a enums name prefix.
     * <p>Uses a common naming idiom: turning all lower case characters to
     * upper case, and prepending upper case characters with an underscore.
     * <p>Example: "imageSize" -> "IMAGE_SIZE"<br>
     * Example: "imagesize" -> "IMAGESIZE".<br>
     * Example: "ImageSize" -> "_IMAGE_SIZE".<br>
     * Example: "IMAGESIZE" -> "_I_M_A_G_E_S_I_Z_E"
     *
     * @param propertyName the name of the bean property
     * @return the corresponding enums name prefix
     * @see #getValuesForProperty
     * @see #toNameForProperty
     */
    public String propertyToConstantNamePrefix(String propertyName) {
        StringBuilder parsedPrefix = new StringBuilder();
        for (int i = 0; i < propertyName.length(); i++) {
            char c = propertyName.charAt(i);
            if (Character.isUpperCase(c)) {
                parsedPrefix.append("_");
                parsedPrefix.append(c);
            } else {
                parsedPrefix.append(Character.toUpperCase(c));
            }
        }
        return parsedPrefix.toString();
    }


    public static class EnumException extends IllegalArgumentException {
        public EnumException(String message) {
            super(message);
        }

        public EnumException(String className, String field, String message) {
            super("Field '" + field + "' " + message + " in class [" + className + "]");
        }

        public EnumException(String className, String namePrefix, Object value) {
            super("No '" + namePrefix + "' field with value '" + value + "' found in class [" + className + "]");
        }

        public EnumException(String className, Object value) {
            super("No value '" + value + "' found in class [" + className + "]");
        }
    }
}
